﻿#region Copyright (c) 2012-10, Olaf Kober <amarok.projects@gmail.com>
//================================================================================
//	Permission is hereby granted, free of charge, to any person obtaining a copy
//	of this software and associated documentation files (the "Software"), to deal
//	in the Software without restriction, including without limitation the rights
//	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//	copies of the Software, and to permit persons to whom the Software is
//	furnished to do so, subject to the following conditions:
//
//	The above copyright notice and this permission notice shall be included in
//	all copies or substantial portions of the Software.
//
//	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//	THE SOFTWARE.
//================================================================================
#endregion

using System;
using System.Collections;
using System.Reflection;
using System.Text;


namespace Amarok
{
	/// <summary>
	/// </summary>
	internal static class ExceptionFormatter
	{
		#region ++ Public Interface ++

		/// <summary>
		/// </summary>
		/// <param name="exception"></param>
		/// <param name="formatProvider"></param>
		/// <returns></returns>
		public static String Format(Exception exception, IFormatProvider formatProvider)
		{
			if (exception == null)
				return null;

			var exceptionStack = new Stack();

			// push all inner exceptions onto stack
			var currentException = exception;
			while (currentException != null)
			{
				exceptionStack.Push(currentException);
				currentException = currentException.InnerException;
			}

			// loop through inner exceptions in reverse order
			var sb = new StringBuilder();
			for (Int32 i = 1; exceptionStack.Count > 0; i++)
			{
				currentException = (Exception)exceptionStack.Pop();
				_WriteException(formatProvider, sb, currentException, i);
			}

			// that's it; return result
			return sb.ToString();
		}

		#endregion

		#region Implementation

		// constants
		private const String __delimiterLine =
			"================================================================================\r\n";
		private const String __innerMostDelimiterLine =
			"=======================================================(inner most exception)===\r\n";
		private const String __unavailable = "<unavailable>";


		private static void _WriteException(IFormatProvider formatProvider, StringBuilder sb, Exception exception,
			Int32 exceptionIndex)
		{
			_WriteHeader(formatProvider, sb, exception, exceptionIndex);
			_WriteDetails(formatProvider, sb, exception);
			_WriteMessage(formatProvider, sb, exception);
			_WriteProperties(formatProvider, sb, exception);
			_WriteData(formatProvider, sb, exception);
			_WriteStackTrace(formatProvider, sb, exception);
			sb.Append(__delimiterLine);
		}

		private static void _WriteHeader(IFormatProvider formatProvider, StringBuilder sb, Exception exception,
			Int32 exceptionIndex)
		{
			//	=======================================================(inner most exception)===
			//	 (index) exception-type-name
			//  ================================================================================
			//
			sb.Append(exceptionIndex == 1 ? __innerMostDelimiterLine : __delimiterLine);
			sb.AppendFormat(formatProvider, " ({0}) {1}\r\n",
				exceptionIndex,
				exception.GetType().FullName);
			sb.Append(__delimiterLine);
		}

		private static void _WriteDetails(IFormatProvider formatProvider, StringBuilder sb, Exception exception)
		{
			//	Method        :  set_Attributes
			//	Type          :  System.IO.FileSystemInfo
			//	Assembly      :  mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
			//	Assembly Path :  C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\mscorlib.dll
			//	Source        :  mscorlib
			//  Helplink      :  <unavailable>
			//
			String assemblyName, assemblyModuleName, typeName, methodName;
			_GetTargetSiteInfo(exception, out assemblyName, out assemblyModuleName, out typeName, out methodName);

			sb.AppendFormat(formatProvider,
				"Method        :  {0}\r\n" +
				"Type          :  {1}\r\n" +
				"Assembly      :  {2}\r\n" +
				"Assembly Path :  {3}\r\n" +
				"Source        :  {4}\r\n" +
				"Helplink      :  {5}\r\n",
				methodName,
				typeName,
				assemblyName,
				assemblyModuleName,
				exception.Source,
				exception.HelpLink);
		}

		private static void _WriteMessage(IFormatProvider formatProvider, StringBuilder sb, Exception exception)
		{
			//	Message:
			//	"..."
			//
			sb.AppendFormat(formatProvider, "\r\nMessage:\r\n\"{0}\"\r\n",
				exception.Message);
		}

		private static void _WriteProperties(IFormatProvider formatProvider, StringBuilder sb, Exception exception)
		{
			//	Properties:
			//	  ArgumentException.ParamName = "text"
			//
			var properties = exception.GetType().GetProperties(BindingFlags.FlattenHierarchy |
				BindingFlags.Instance | BindingFlags.Public);

			var first = true;
			foreach (var property in properties)
			{
				if (property.DeclaringType == typeof(Exception))
					continue;
				if (property.Name.Equals("Message"))
					continue;

				// write header
				if (first)
				{
					first = false;
					sb.Append("\r\nProperties:\r\n");
				}

				// obtain property value
				var propertyValue = (Object)__unavailable;

				if (property.CanRead)
					propertyValue = property.GetValue(exception, null);

				// determine whether property returned a collection
				var enumerableValue = propertyValue as IEnumerable;

				if (propertyValue is String)
				{
					// special handling for strings
					sb.AppendFormat(formatProvider, "  {0}.{1} = \"{2}\"\r\n",
						property.ReflectedType.Name,
						property.Name,
						propertyValue);
				}
				else if (enumerableValue != null)
				{
					// special handling for collections
					sb.AppendFormat(formatProvider, "  {0}.{1} = {{\r\n",
						property.ReflectedType.Name,
						property.Name);

					foreach (var item in enumerableValue)
					{
						sb.AppendFormat(formatProvider, "    \"{0}\",\r\n",
							item != null ? item.ToString() : String.Empty);
					}

					sb.Append("  }\r\n");
				}
				else
				{
					// anything else
					sb.AppendFormat(formatProvider, "  {0}.{1} = {{ {2} }}\r\n",
						property.ReflectedType.Name,
						property.Name,
						propertyValue);
				}
			}
		}

		private static void _WriteData(IFormatProvider formatProvider, StringBuilder sb, Exception exception)
		{
			//	Data:
			//	  Name = "value"
			//
			if (exception.Data == null || exception.Data.Count <= 0)
				return;

			sb.Append("\r\nData:\r\n");
			foreach (DictionaryEntry entry in exception.Data)
			{
				sb.AppendFormat(formatProvider, "{0} = \"{1}\"\r\n",
					entry.Key,
					entry.Value);
			}
		}

		private static void _WriteStackTrace(IFormatProvider formatProvider, StringBuilder sb, Exception exception)
		{
			//	Stack Trace:
			//	  at System.IO.FileSystemInfo.set_Attributes(FileAttributes value)
			//    at Amarok.Logging.LogStoreWriter._SetupRootFolder() 
			//    at Amarok.Logging.LogStoreWriter..ctor(String rootPath) 
			//
			sb.AppendFormat(formatProvider, "\r\nStack Trace:\r\n{0}\r\n",
				exception.StackTrace);
		}

		private static void _GetTargetSiteInfo(Exception exception, out String assemblyName,
			out String assemblyModulePath, out String typeName, out String methodName)
		{
			// set defaults
			assemblyName = __unavailable;
			assemblyModulePath = __unavailable;
			typeName = __unavailable;
			methodName = __unavailable;

			// safely obtain values
			var targetSite = exception.TargetSite;

			if (targetSite == null)
				return;

			methodName = targetSite.Name;
			var type = targetSite.ReflectedType;

			if (type == null)
				return;

			typeName = type.FullName;
			var assembly = type.Assembly;

			if (assembly == null)
				return;

			assemblyName = assembly.FullName;
			var assemblyModule = assembly.ManifestModule;

			if (assemblyModule == null)
				return;

			assemblyModulePath = assemblyModule.FullyQualifiedName;
		}

		#endregion

	}
}
